﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Xml;
using stunnware.CRM.Fetch;

namespace LinqtoCRM
{

    public class CrmQueryProvider : QueryProvider
    {
        ICrmService _service;
        public CrmQueryProvider(ICrmService service)
        {
            _service = service;
        }

        public TextWriter Log
        {
            get;
            set;
        }
        public IQueryable<T> Linq<T>()
        {
            return new Query<T>(this);
        }

        private void WriteLog(string message)
        {
            if (Log != null)
            {
                Log.WriteLine(message);
            }
        }

        /// <summary>
        /// This method is apparently used by the debugger to visualize the current query
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public override string GetQueryText(Expression expression)
        {
            return this.Translate(expression).FetchXML;
        }

        public override object Execute(Expression expression)
        {
            TranslateResult translateResult = this.Translate(expression);
            WriteLog(translateResult.FetchXML);
            string resultXml = _service.Fetch(translateResult.FetchXML);
            WriteLog(resultXml);
            XmlDocument xmldoc = new XmlDocument();
            xmldoc.LoadXml(resultXml);
            if (xmldoc.SelectNodes("/resultset/result/count") != null)
            {
                return Int32.Parse(xmldoc.SelectSingleNode("/resultset/result/count").FirstChild.Value);
            }
            else
            {
                throw new Exception("Error executing query");
            }
        }

        internal XmlNode CurrentNode { get; private set; }
        internal TranslateResult CurrentTransResult { get; private set; }

        private Dictionary<Type, object> _currentObjects = new Dictionary<Type, object>();

        internal object GetCurrentCrmObject(Type t)
        {
            object o;
            _currentObjects.TryGetValue(t, out o);
            if (o == null)
            {
                o = ConstructCrmObject(t);
                _currentObjects.Add(t, o);
            }
            return o;
        }

        private object ConstructCrmObject(Type t)
        {

            ConstructorInfo ctor = t.GetConstructor(Type.EmptyTypes);
            if (ctor == null)
            {
                throw new ArgumentException(string.Format("Type {0] doesn't have a parameterless constructor. Not a businessentity?", t));
            }
            object obj = ctor.Invoke(null);
            XmlNodeList attrNodes = null;
            string prefix = string.Empty;

            FetchStatement stmt = CurrentTransResult.FetchStatement;

            if (t.Name == stmt.Entity.EntityName)
            {
                attrNodes = CurrentNode.SelectNodes("./*[not(contains(name(),'.'))]");
            }
            else
            {
                foreach (FetchLinkEntity linked in stmt.GetEntities().Where(e => e.EntityName == t.Name))
                {
                    prefix = linked.To + ".";
                    attrNodes = CurrentNode.SelectNodes(string.Format("./*[starts-with(name(),'{0}')]", prefix));
                }
            }

            if (attrNodes == null || attrNodes.Count == 0)
            {
                throw new ArgumentException(string.Format("No value nodes found for Type {0}", t));
            }

            foreach (XmlNode attrNode in attrNodes)
            {
                string propName = attrNode.Name.Substring(prefix.Length);
                PropertyInfo propInfo = t.GetProperty(propName);
                if (propInfo != null)
                {
                    propInfo.SetValue(obj, CreateValueFromNode(propInfo, attrNode), null);
                }
            }

            return obj;
        }

        internal IEnumerator<TResult> ExecuteQuery<TResult>(Expression expression)
        {
            TranslateResult transResult = this.Translate(expression);
            WriteLog(transResult.FetchXML);
            string result = _service.Fetch(transResult.FetchXML);

            if (result == "<resultset morerecords=\"0\" />")
                yield break;

            XmlDocument xmldoc = new XmlDocument();
            // todo, use lazy loading?
            xmldoc.LoadXml(result);
            XmlNodeList nodelist = xmldoc.SelectNodes("/resultset/result");

            CurrentTransResult = transResult;

            foreach (XmlNode resultNode in nodelist)
            {
                // Setup current state
                CurrentNode = resultNode;
                _currentObjects.Clear();

                // project
                object projectedResult = null;
                try
                {
                    projectedResult = transResult.Projector.DynamicInvoke();
                }
                catch (TargetInvocationException targetException)
                {
                    throw new Exception("Failed to project selection.", targetException);
                }

                yield return (TResult)projectedResult;
            }
        }

        internal class TranslateResult
        {
            internal FetchStatement FetchStatement;
            internal Delegate Projector;
            internal string FetchXML { get { return FetchStatement.ToString(); } }
        }

        private TranslateResult Translate(Expression expression)
        {
            expression = Evaluator.PartialEval(expression);
            QueryFormatter formatter = new QueryFormatter(this);

            //LambdaExpression projector = new ProjectionBuilder().Build(proj.Projector);
            return new TranslateResult { FetchStatement = formatter.Format(expression), Projector = formatter.Projector };
            //return null;
        }

        private object CreateValueFromNode(PropertyInfo propertyInfo, XmlNode valueNode)
        {
            string nodeText = valueNode.InnerText;

            if (propertyInfo.PropertyType == typeof(string))
            {
                return nodeText;
            }
            else
            {
                // This handles the initialization of property-types, much improved by Mel
                object property = Activator.CreateInstance(propertyInfo.PropertyType);
                Type t = propertyInfo.PropertyType.GetProperty("Value").PropertyType;
                propertyInfo.PropertyType.GetProperty("Value").SetValue(property, CreateValueProperty(t, nodeText), null);

                string[] propertyNames = { "name", "formattedvalue", "time", "date", "type" };

                foreach (string propertyName in propertyNames)
                {
                    if (valueNode.Attributes[propertyName] != null && propertyInfo.PropertyType.GetProperty(propertyName) != null)
                    {
                        PropertyInfo pi = propertyInfo.PropertyType.GetProperty(propertyName);
                        pi.SetValue(property, valueNode.Attributes[propertyName].Value, null);
                    }
                }
                return property;
            }
        }

        private object CreateValueProperty(Type type, string value)
        {
            if (type.IsEnum)
            {
                return Enum.Parse(type, value);
            }
            
			switch (type.Name)
            {
                case "Boolean": return (value == "1");
                case "String": return value;
                case "Guid": return new Guid(value);
                case "Int32": return XmlConvert.ToInt32(value);
                case "Decimal": return XmlConvert.ToDecimal(value);
				case "Double": return XmlConvert.ToDouble(value);
				default : throw new NotSupportedException(type.Name);
            }
        }
    }
}
